/*-------------------------------------------------------------------------
 *
 * pl_comp.c		- PL/pgSQL 的编译器部分
 *			  过程语言
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/pl/plpgsql/src/pl_comp.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <ctype.h>

#include "access/htup_details.h"
#include "catalog/namespace.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "funcapi.h"
#include "nodes/makefuncs.h"
#include "parser/parse_type.h"
#include "plpgsql.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/regproc.h"
#include "utils/rel.h"
#include "utils/syscache.h"
#include "utils/typcache.h"

/* ----------
 * 我们自己的本地和全局变量
 * ----------
 */
PLpgSQL_stmt_block *plpgsql_parse_result;

static int	datums_alloc;
int			plpgsql_nDatums;
PLpgSQL_datum **plpgsql_Datums;
static int	datums_last;

char	   *plpgsql_error_funcname;
bool		plpgsql_DumpExecTree = false;
bool		plpgsql_check_syntax = false;

PLpgSQL_function *plpgsql_curr_compile;

/* 适用于编译期间短期分配的上下文 */
MemoryContext plpgsql_compile_tmp_cxt;

/* ----------
 * 编译函数的哈希表
 * ----------
 */
static HTAB *plpgsql_HashTable = NULL;

typedef struct plpgsql_hashent
{
	PLpgSQL_func_hashkey key;
	PLpgSQL_function *function;
} plpgsql_HashEnt;

#define FUNCS_PER_USER		128 /* 初始表大小 */

/* ----------
 * EXCEPTION 条件名称的查找表
 * ----------
 */
typedef struct
{
	const char *label;
	int			sqlerrstate;
} ExceptionLabelMap;

static const ExceptionLabelMap exception_label_map[] = {
#include "plerrcodes.h"			/* pgrminclude 忽略 */
	{NULL, 0}
};


/* ----------
 * 静态原型
 * ----------
 */
static PLpgSQL_function *fc_do_compile(FunctionCallInfo fc_fcinfo,
									HeapTuple fc_procTup,
									PLpgSQL_function *fc_function,
									PLpgSQL_func_hashkey *fc_hashkey,
									bool fc_forValidator);
static void fc_plpgsql_compile_error_callback(void *fc_arg);
static void fc_add_parameter_name(PLpgSQL_nsitem_type fc_itemtype, int fc_itemno, const char *fc_name);
static void fc_add_dummy_return(PLpgSQL_function *fc_function);
static Node *fc_plpgsql_pre_column_ref(ParseState *fc_pstate, ColumnRef *fc_cref);
static Node *fc_plpgsql_post_column_ref(ParseState *fc_pstate, ColumnRef *fc_cref, Node *fc_var);
static Node *fc_plpgsql_param_ref(ParseState *fc_pstate, ParamRef *fc_pref);
static Node *fc_resolve_column_ref(ParseState *fc_pstate, PLpgSQL_expr *fc_expr,
								ColumnRef *fc_cref, bool fc_error_if_no_field);
static Node *fc_make_datum_param(PLpgSQL_expr *fc_expr, int fc_dno, int fc_location);
static PLpgSQL_row *fc_build_row_from_vars(PLpgSQL_variable **fc_vars, int fc_numvars);
static PLpgSQL_type *fc_build_datatype(HeapTuple fc_typeTup, int32 fc_typmod,
									Oid fc_collation, TypeName *fc_origtypname);
static void fc_plpgsql_start_datums(void);
static void fc_plpgsql_finish_datums(PLpgSQL_function *fc_function);
static void fc_compute_function_hashkey(FunctionCallInfo fc_fcinfo,
									 Form_pg_proc fc_procStruct,
									 PLpgSQL_func_hashkey *fc_hashkey,
									 bool fc_forValidator);
static void fc_plpgsql_resolve_polymorphic_argtypes(int fc_numargs,
												 Oid *fc_argtypes, char *fc_argmodes,
												 Node *fc_call_expr, bool fc_forValidator,
												 const char *fc_proname);
static PLpgSQL_function *fc_plpgsql_HashTableLookup(PLpgSQL_func_hashkey *fc_func_key);
static void fc_plpgsql_HashTableInsert(PLpgSQL_function *fc_function,
									PLpgSQL_func_hashkey *fc_func_key);
static void fc_plpgsql_HashTableDelete(PLpgSQL_function *fc_function);
static void fc_delete_function(PLpgSQL_function *fc_func);

/* ----------
 * plpgsql_compile		为 PL/pgSQL 函数生成执行树。
 *
 * 如果 forValidator 为真，我们只是为了验证目的进行编译，
 * 因此会跳过一些检查。
 *
 * 注意：如果函数已经被编译，这个过程快速完成是很重要的。
 * ----------
 */
PLpgSQL_function *
plpgsql_compile(FunctionCallInfo fc_fcinfo, bool fc_forValidator)
{
	Oid			fc_funcOid = fc_fcinfo->flinfo->fn_oid;
	HeapTuple	fc_procTup;
	Form_pg_proc fc_procStruct;
	PLpgSQL_function *fc_function;
	PLpgSQL_func_hashkey fc_hashkey;
	bool		fc_function_valid = false;
	bool		fc_hashkey_valid = false;

	/*
	 * 通过 Oid 查找 pg_proc 元组；在任何情况下我们都需要它
	 */
	fc_procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(fc_funcOid));
	if (!HeapTupleIsValid(fc_procTup))
		elog(ERROR, "cache lookup failed for function %u", fc_funcOid);
	fc_procStruct = (Form_pg_proc) GETSTRUCT(fc_procTup);

	/*
	 * 检查当前 FmgrInfo 是否已经有缓存条目。如果没有，
	 * 尝试在哈希表中找到一个。
	 */
	fc_function = (PLpgSQL_function *) fc_fcinfo->flinfo->fn_extra;

recheck:
	if (!fc_function)
	{
		/* 使用函数签名和实际参数类型计算哈希键 */
		fc_compute_function_hashkey(fc_fcinfo, fc_procStruct, &fc_hashkey, fc_forValidator);
		fc_hashkey_valid = true;

		/* 然后进行查找 */
		fc_function = fc_plpgsql_HashTableLookup(&fc_hashkey);
	}

	if (fc_function)
	{
		/* 我们有一个编译的函数，但它仍然有效吗？ */
		if (fc_function->fn_xmin == HeapTupleHeaderGetRawXmin(fc_procTup->t_data) &&
			ItemPointerEquals(&fc_function->fn_tid, &fc_procTup->t_self))
			fc_function_valid = true;
		else
		{
			/*
			 * 不行，所以从哈希表中删除它，并尝试删除关联的
			 * 存储（如果尚未完成）。
			 */
			fc_delete_function(fc_function);

			/*
			 * 如果该函数不是在活跃使用中，那么我们可以用新数据
			 * 覆盖 func 结构，允许任何其他现有的 fn_extra
			 * 指针在下次使用时利用新的定义。
			 * 如果正在使用，则保持不变并创建一个新函数。
			 * （活跃调用将使用之前的定义运行到完成，然后缓存条目
			 * 将被泄露；考虑到这是一个边缘情况，似乎不值得添加代码
			 * 来清理它。）
			 *
			 * 如果我们通过 fn_extra 找到了函数结构，可能已经
			 * 进行了替换，因此返回并重新检查哈希表。
			 */
			if (fc_function->use_count != 0)
			{
				fc_function = NULL;
				if (!fc_hashkey_valid)
					goto recheck;
			}
		}
	}

	/*
	 * 如果没有找到函数或者它已过时，我们需要编译它
	 */
	if (!fc_function_valid)
	{
		/*
		 * 如果我们还没有计算哈希键，则计算它；我们需要它来存储
		 * 完成的函数。
		 */
		if (!fc_hashkey_valid)
			fc_compute_function_hashkey(fc_fcinfo, fc_procStruct, &fc_hashkey,
									 fc_forValidator);

		/*
		 * 做艰难的部分。
		 */
		fc_function = fc_do_compile(fc_fcinfo, fc_procTup, fc_function,
							  &fc_hashkey, fc_forValidator);
	}

	ReleaseSysCache(fc_procTup);

	/*
	 * 在 FmgrInfo 中保存指针，以避免后续调用时搜索
	 */
	fc_fcinfo->flinfo->fn_extra = (void *) fc_function;

	/*
	 * 最后返回编译的函数
	 */
	return fc_function;
}

/*
 * 这是 plpgsql_compile() 的慢速部分。
 *
 * 传入的“function”指针要么为 NULL，要么是已分配的
 * 函数结构体，用于覆盖。
 *
 * 在编译函数时，CurrentMemoryContext 是
 * 我们正在编译的函数的每个函数内存上下文。这
 * 意味着 palloc() 将分配与
 * 函数本身相同生命周期的存储空间。
 *
 * 由于 palloc() 分配的存储不会立即释放，临时
 * 分配应在短期内存上下文中进行或显式调用 pfree。由于并非所有后端函数都
 * 小心处理 pfree 他们的分配，因此在调用后端
 * 之前切换到短期上下文也是明智的。适合进行短期
 * 分配的上下文是 plpgsql_compile_tmp_cxt。
 *
 * 注意：此代码不是可重入的。我们假设我们在这里做的任何事情都不会
 * 导致调用另一个 plpgsql 函数。
 */
static PLpgSQL_function *
fc_do_compile(FunctionCallInfo fc_fcinfo,
		   HeapTuple fc_procTup,
		   PLpgSQL_function *fc_function,
		   PLpgSQL_func_hashkey *fc_hashkey,
		   bool fc_forValidator)
{
	Form_pg_proc fc_procStruct = (Form_pg_proc) GETSTRUCT(fc_procTup);
	bool		fc_is_dml_trigger = CALLED_AS_TRIGGER(fc_fcinfo);
	bool		fc_is_event_trigger = CALLED_AS_EVENT_TRIGGER(fc_fcinfo);
	Datum		fc_prosrcdatum;
	bool		fc_isnull;
	char	   *fc_proc_source;
	HeapTuple	fc_typeTup;
	Form_pg_type fc_typeStruct;
	PLpgSQL_variable *fc_var;
	PLpgSQL_rec *fc_rec;
	int			fc_i;
	ErrorContextCallback fc_plerrcontext;
	int			fc_parse_rc;
	Oid			fc_rettypeid;
	int			fc_numargs;
	int			fc_num_in_args = 0;
	int			fc_num_out_args = 0;
	Oid		   *fc_argtypes;
	char	  **fc_argnames;
	char	   *fc_argmodes;
	int		   *fc_in_arg_varnos = NULL;
	PLpgSQL_variable **fc_out_arg_variables;
	MemoryContext fc_func_cxt;

	/*
	 * 设置扫描器输入和错误信息。我们假设此函数
	 * 不能递归调用，因此无需保存和恢复
	 * 此处使用的静态变量。
	 */
	fc_prosrcdatum = SysCacheGetAttr(PROCOID, fc_procTup,
								  Anum_pg_proc_prosrc, &fc_isnull);
	if (fc_isnull)
		elog(ERROR, "null prosrc");
	fc_proc_source = TextDatumGetCString(fc_prosrcdatum);
	plpgsql_scanner_init(fc_proc_source);

	plpgsql_error_funcname = pstrdup(NameStr(fc_procStruct->proname));

	/*
	 * 为 ereport() 设置错误回溯支持。
	 */
	fc_plerrcontext.callback = fc_plpgsql_compile_error_callback;
	fc_plerrcontext.arg = fc_forValidator ? fc_proc_source : NULL;
	fc_plerrcontext.previous = error_context_stack;
	error_context_stack = &fc_plerrcontext;

	/*
	 * 在验证函数定义时进行额外的语法检查。当实际编译函数以执行时，我们跳过
	 * 这一步，出于性能原因。
	 */
	plpgsql_check_syntax = fc_forValidator;

	/*
	 * 创建新的函数结构体，如果尚未完成。函数
	 * 结构体永远不会被丢弃，因此将它们保存在 TopMemoryContext 中。
	 */
	if (fc_function == NULL)
	{
		fc_function = (PLpgSQL_function *)
			MemoryContextAllocZero(TopMemoryContext, sizeof(PLpgSQL_function));
	}
	else
	{
		/* 重新使用先前存在的结构，因此清除它 */
		memset(fc_function, 0, sizeof(PLpgSQL_function));
	}
	plpgsql_curr_compile = fc_function;

	/*
	 * 编译的所有永久输出（例如，解析树）保存在一个
	 * 每个函数的内存上下文中，以便可以轻松回收。
	 */
	fc_func_cxt = AllocSetContextCreate(TopMemoryContext,
									 "PL/pgSQL function",
									 ALLOCSET_DEFAULT_SIZES);
	plpgsql_compile_tmp_cxt = MemoryContextSwitchTo(fc_func_cxt);

	fc_function->fn_signature = format_procedure(fc_fcinfo->flinfo->fn_oid);
	MemoryContextSetIdentifier(fc_func_cxt, fc_function->fn_signature);
	fc_function->fn_oid = fc_fcinfo->flinfo->fn_oid;
	fc_function->fn_xmin = HeapTupleHeaderGetRawXmin(fc_procTup->t_data);
	fc_function->fn_tid = fc_procTup->t_self;
	fc_function->fn_input_collation = fc_fcinfo->fncollation;
	fc_function->fn_cxt = fc_func_cxt;
	fc_function->out_param_varno = -1; /* 设置没有 OUT 参数 */
	fc_function->resolve_option = plpgsql_variable_conflict;
	fc_function->print_strict_params = plpgsql_print_strict_params;
	/* 仅在创建函数时提升额外的警告和错误 */
	fc_function->extra_warnings = fc_forValidator ? plpgsql_extra_warnings : 0;
	fc_function->extra_errors = fc_forValidator ? plpgsql_extra_errors : 0;

	if (fc_is_dml_trigger)
		fc_function->fn_is_trigger = PLPGSQL_DML_TRIGGER;
	else if (fc_is_event_trigger)
		fc_function->fn_is_trigger = PLPGSQL_EVENT_TRIGGER;
	else
		fc_function->fn_is_trigger = PLPGSQL_NOT_TRIGGER;

	fc_function->fn_prokind = fc_procStruct->prokind;

	fc_function->nstatements = 0;
	fc_function->requires_procedure_resowner = false;

	/*
	 * 初始化编译器，特别是命名空间堆栈。
	 * 外层命名空间包含函数参数和其他特殊
	 * 变量（如 FOUND），并以函数本身命名。
	 */
	plpgsql_ns_init();
	plpgsql_ns_push(NameStr(fc_procStruct->proname), PLPGSQL_LABEL_BLOCK);
	plpgsql_DumpExecTree = false;
	fc_plpgsql_start_datums();

	switch (fc_function->fn_is_trigger)
	{
		case PLPGSQL_NOT_TRIGGER:

			/*
			 * 获取过程参数的信息。分配不需要永久，
			 * 所以在临时上下文中进行。
			 *
			 * 我们还需要解析任何多态输入或输出
			 * 参数类型。在验证模式中，我们将无法做到这一点，因此我们
			 * 任意假设我们处理的是整数。
			 */
			MemoryContextSwitchTo(plpgsql_compile_tmp_cxt);

			fc_numargs = get_func_arg_info(fc_procTup,
										&fc_argtypes, &fc_argnames, &fc_argmodes);

			fc_plpgsql_resolve_polymorphic_argtypes(fc_numargs, fc_argtypes, fc_argmodes,
												 fc_fcinfo->flinfo->fn_expr,
												 fc_forValidator,
												 plpgsql_error_funcname);

			fc_in_arg_varnos = (int *) palloc(fc_numargs * sizeof(int));
			fc_out_arg_variables = (PLpgSQL_variable **) palloc(fc_numargs * sizeof(PLpgSQL_variable *));

			MemoryContextSwitchTo(fc_func_cxt);

			/*
			 * 为过程参数创建变量。
			 */
			for (fc_i = 0; fc_i < fc_numargs; fc_i++)
			{
				char		fc_buf[32];
				Oid			fc_argtypeid = fc_argtypes[fc_i];
				char		fc_argmode = fc_argmodes ? fc_argmodes[fc_i] : PROARGMODE_IN;
				PLpgSQL_type *fc_argdtype;
				PLpgSQL_variable *fc_argvariable;
				PLpgSQL_nsitem_type fc_argitemtype;

				/* 为变量创建 $n 名称 */
				snprintf(fc_buf, sizeof(fc_buf), "$%d", fc_i + 1);

				/* 创建数据类型信息 */
				fc_argdtype = plpgsql_build_datatype(fc_argtypeid,
												  -1,
												  fc_function->fn_input_collation,
												  NULL);

				/* 不允许伪类型参数 */
				/* （注意我们已经替换了多态类型） */
				/* （build_variable 会这样做，但消息不正确） */
				if (fc_argdtype->ttype == PLPGSQL_TTYPE_PSEUDO)
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("PL/pgSQL functions cannot accept type %s",
									format_type_be(fc_argtypeid))));

				/*
				 * 构建变量并添加到数据列表。如果参数有名称，
				 * 则使用该名称作为 refname，否则使用 $n 名称。
				 */
				fc_argvariable = plpgsql_build_variable((fc_argnames &&
													  fc_argnames[fc_i][0] != '\0') ?
													 fc_argnames[fc_i] : fc_buf,
													 0, fc_argdtype, false);

				if (fc_argvariable->dtype == PLPGSQL_DTYPE_VAR)
				{
					fc_argitemtype = PLPGSQL_NSTYPE_VAR;
				}
				else
				{
					Assert(fc_argvariable->dtype == PLPGSQL_DTYPE_REC);
					fc_argitemtype = PLPGSQL_NSTYPE_REC;
				}

				/* 在适当的数组中记住参数 */
				if (fc_argmode == PROARGMODE_IN ||
					fc_argmode == PROARGMODE_INOUT ||
					fc_argmode == PROARGMODE_VARIADIC)
					fc_in_arg_varnos[fc_num_in_args++] = fc_argvariable->dno;
				if (fc_argmode == PROARGMODE_OUT ||
					fc_argmode == PROARGMODE_INOUT ||
					fc_argmode == PROARGMODE_TABLE)
					fc_out_arg_variables[fc_num_out_args++] = fc_argvariable;

				/* 在 $n 名称下添加到命名空间 */
				fc_add_parameter_name(fc_argitemtype, fc_argvariable->dno, fc_buf);

				/* 如果参数有名称，则创建别名 */
				if (fc_argnames && fc_argnames[fc_i][0] != '\0')
					fc_add_parameter_name(fc_argitemtype, fc_argvariable->dno,
									   fc_argnames[fc_i]);
			}

			/*
			 * 如果只有一个 OUT 参数，out_param_varno 直接指向它。
			 * 如果有多个，则构建一个包含所有 OUT 参数的行。
			 * 无论有几个 OUT 参数，过程都返回一行。
			 */
			if (fc_num_out_args > 1 ||
				(fc_num_out_args == 1 && fc_function->fn_prokind == PROKIND_PROCEDURE))
			{
				PLpgSQL_row *fc_row = fc_build_row_from_vars(fc_out_arg_variables,
													   fc_num_out_args);

				plpgsql_adddatum((PLpgSQL_datum *) fc_row);
				fc_function->out_param_varno = fc_row->dno;
			}
			else if (fc_num_out_args == 1)
				fc_function->out_param_varno = fc_out_arg_variables[0]->dno;

			/*
			 * 检查是否为多态返回类型。如果找到，使用来自调用者的 FuncExpr 节点的实际返回类型，
			 * （如果我们有一个的话）。在验证模式下，我们任意假设处理的是整数。
			 *
			 * 注意：错误代码为 FEATURE_NOT_SUPPORTED，因为它应该始终有效；
			 * 如果无效，我们处于某种无法提供信息的上下文中。
			 */
			fc_rettypeid = fc_procStruct->prorettype;
			if (IsPolymorphicType(fc_rettypeid))
			{
				if (fc_forValidator)
				{
					if (fc_rettypeid == ANYARRAYOID ||
						fc_rettypeid == ANYCOMPATIBLEARRAYOID)
						fc_rettypeid = INT4ARRAYOID;
					else if (fc_rettypeid == ANYRANGEOID ||
							 fc_rettypeid == ANYCOMPATIBLERANGEOID)
						fc_rettypeid = INT4RANGEOID;
					else if (fc_rettypeid == ANYMULTIRANGEOID)
						fc_rettypeid = INT4MULTIRANGEOID;
					else		/* ANYELEMENT 或 ANYNONARRAY 或 ANYCOMPATIBLE */
						fc_rettypeid = INT4OID;
					/* XXX 我们可以为 ANYENUM 使用什么？ */
				}
				else
				{
					fc_rettypeid = get_fn_expr_rettype(fc_fcinfo->flinfo);
					if (!OidIsValid(fc_rettypeid))
						ereport(ERROR,
								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
								 errmsg("could not determine actual return type "
										"for polymorphic function \"%s\"",
										plpgsql_error_funcname)));
				}
			}

			/*
			 * 普通函数有一个定义的返回类型
			 */
			fc_function->fn_rettype = fc_rettypeid;
			fc_function->fn_retset = fc_procStruct->proretset;

			/*
			 * 查找函数的返回类型
			 */
			fc_typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(fc_rettypeid));
			if (!HeapTupleIsValid(fc_typeTup))
				elog(ERROR, "cache lookup failed for type %u", fc_rettypeid);
			fc_typeStruct = (Form_pg_type) GETSTRUCT(fc_typeTup);

			/* 不允许伪类型返回，除了 VOID 或 RECORD */
			/* （注意我们已经替换了多态类型） */
			if (fc_typeStruct->typtype == TYPTYPE_PSEUDO)
			{
				if (fc_rettypeid == VOIDOID ||
					fc_rettypeid == RECORDOID)
					 /* 可以 */ ;
				else if (fc_rettypeid == TRIGGEROID || fc_rettypeid == EVENT_TRIGGEROID)
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("trigger functions can only be called as triggers")));
				else
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("PL/pgSQL functions cannot return type %s",
									format_type_be(fc_rettypeid))));
			}

			fc_function->fn_retistuple = type_is_rowtype(fc_rettypeid);
			fc_function->fn_retisdomain = (fc_typeStruct->typtype == TYPTYPE_DOMAIN);
			fc_function->fn_retbyval = fc_typeStruct->typbyval;
			fc_function->fn_rettyplen = fc_typeStruct->typlen;

			/*
			 * 安装 $0 引用，但仅限于多态返回类型，
			 * 在通过输出参数指定返回类型时不使用。
			 */
			if (IsPolymorphicType(fc_procStruct->prorettype) &&
				fc_num_out_args == 0)
			{
				(void) plpgsql_build_variable("$0", 0,
											  fc_build_datatype(fc_typeTup,
															 -1,
															 fc_function->fn_input_collation,
															 NULL),
											  true);
			}

			ReleaseSysCache(fc_typeTup);
			break;

		case PLPGSQL_DML_TRIGGER:
			/* 触发过程的返回类型尚未确定 */
			fc_function->fn_rettype = InvalidOid;
			fc_function->fn_retbyval = false;
			fc_function->fn_retistuple = true;
			fc_function->fn_retisdomain = false;
			fc_function->fn_retset = false;

			/* 不应有任何声明的参数 */
			if (fc_procStruct->pronargs != 0)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
						 errmsg("trigger functions cannot have declared arguments"),
						 errhint("The arguments of the trigger can be accessed through TG_NARGS and TG_ARGV instead.")));

			/* 添加引用 NEW ROW 的记录 */
			fc_rec = plpgsql_build_record("new", 0, NULL, RECORDOID, true);
			fc_function->new_varno = fc_rec->dno;

			/* 添加引用 OLD ROW 的记录 */
			fc_rec = plpgsql_build_record("old", 0, NULL, RECORDOID, true);
			fc_function->old_varno = fc_rec->dno;

			/* 添加变量 tg_name */
			fc_var = plpgsql_build_variable("tg_name", 0,
										 plpgsql_build_datatype(NAMEOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_NAME;

			/* 添加变量 tg_when */
			fc_var = plpgsql_build_variable("tg_when", 0,
										 plpgsql_build_datatype(TEXTOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_WHEN;

			/* 添加变量 tg_level */
			fc_var = plpgsql_build_variable("tg_level", 0,
										 plpgsql_build_datatype(TEXTOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_LEVEL;

			/* 添加变量 tg_op */
			fc_var = plpgsql_build_variable("tg_op", 0,
										 plpgsql_build_datatype(TEXTOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_OP;

			/* 添加变量 tg_relid */
			fc_var = plpgsql_build_variable("tg_relid", 0,
										 plpgsql_build_datatype(OIDOID,
																-1,
																InvalidOid,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_RELID;

			/* 添加变量 tg_relname */
			fc_var = plpgsql_build_variable("tg_relname", 0,
										 plpgsql_build_datatype(NAMEOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_TABLE_NAME;

			/* tg_table_name 现在优先于 tg_relname */
			fc_var = plpgsql_build_variable("tg_table_name", 0,
										 plpgsql_build_datatype(NAMEOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_TABLE_NAME;

			/* 添加变量 tg_table_schema */
			fc_var = plpgsql_build_variable("tg_table_schema", 0,
										 plpgsql_build_datatype(NAMEOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_TABLE_SCHEMA;

			/* 添加变量 tg_nargs */
			fc_var = plpgsql_build_variable("tg_nargs", 0,
										 plpgsql_build_datatype(INT4OID,
																-1,
																InvalidOid,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_NARGS;

			/* 添加变量 tg_argv */
			fc_var = plpgsql_build_variable("tg_argv", 0,
										 plpgsql_build_datatype(TEXTARRAYOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_ARGV;

			break;

		case PLPGSQL_EVENT_TRIGGER:
			fc_function->fn_rettype = VOIDOID;
			fc_function->fn_retbyval = false;
			fc_function->fn_retistuple = true;
			fc_function->fn_retisdomain = false;
			fc_function->fn_retset = false;

			/* 不应有任何声明的参数 */
			if (fc_procStruct->pronargs != 0)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
						 errmsg("event trigger functions cannot have declared arguments")));

			/* 添加变量 tg_event */
			fc_var = plpgsql_build_variable("tg_event", 0,
										 plpgsql_build_datatype(TEXTOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_EVENT;

			/* 添加变量 tg_tag */
			fc_var = plpgsql_build_variable("tg_tag", 0,
										 plpgsql_build_datatype(TEXTOID,
																-1,
																fc_function->fn_input_collation,
																NULL),
										 true);
			Assert(fc_var->dtype == PLPGSQL_DTYPE_VAR);
			fc_var->dtype = PLPGSQL_DTYPE_PROMISE;
			((PLpgSQL_var *) fc_var)->promise = PLPGSQL_PROMISE_TG_TAG;

			break;

		default:
			elog(ERROR, "unrecognized function typecode: %d",
				 (int) fc_function->fn_is_trigger);
			break;
	}

	/* 记住函数是否为 STABLE/IMMUTABLE */
	fc_function->fn_readonly = (fc_procStruct->provolatile != PROVOLATILE_VOLATILE);

	/*
	 * 创建魔术 FOUND 变量。
	 */
	fc_var = plpgsql_build_variable("found", 0,
								 plpgsql_build_datatype(BOOLOID,
														-1,
														InvalidOid,
														NULL),
								 true);
	fc_function->found_varno = fc_var->dno;

	/*
	 * 现在解析函数的文本
	 */
	fc_parse_rc = plpgsql_yyparse();
	if (fc_parse_rc != 0)
		elog(ERROR, "plpgsql parser returned %d", fc_parse_rc);
	fc_function->action = plpgsql_parse_result;

	plpgsql_scanner_finish();
	pfree(fc_proc_source);

	/*
	 * 如果它有 OUT 参数或返回 VOID 或返回集合，我们允许控制在没有
	 * 显式 RETURN 语句的情况下掉落到末尾。实现此功能的最简单方法是在解析期间
	 * 将 RETURN 语句添加到语句列表的末尾。
	 */
	if (fc_num_out_args > 0 || fc_function->fn_rettype == VOIDOID ||
		fc_function->fn_retset)
		fc_add_dummy_return(fc_function);

	/*
	 * 完成函数的信息
	 */
	fc_function->fn_nargs = fc_procStruct->pronargs;
	for (fc_i = 0; fc_i < fc_function->fn_nargs; fc_i++)
		fc_function->fn_argvarnos[fc_i] = fc_in_arg_varnos[fc_i];

	fc_plpgsql_finish_datums(fc_function);

	/* 已完成函数的调试转储 */
	if (plpgsql_DumpExecTree)
		plpgsql_dumptree(fc_function);

	/*
	 * 将其添加到哈希表中
	 */
	fc_plpgsql_HashTableInsert(fc_function, fc_hashkey);

	/*
	 * 弹出错误上下文堆栈
	 */
	error_context_stack = fc_plerrcontext.previous;
	plpgsql_error_funcname = NULL;

	plpgsql_check_syntax = false;

	MemoryContextSwitchTo(plpgsql_compile_tmp_cxt);
	plpgsql_compile_tmp_cxt = NULL;
	return fc_function;
}

/* ----------
 * plpgsql_compile_inline	为匿名代码块创建执行树。
 *
 * 注意：这通常与 do_compile() 并行；是否值得尝试合并这两者？
 *
 * 注意：我们假设该块将被丢弃，因此没有必要构建持久的数据结构。
 * ----------
 */
PLpgSQL_function *
plpgsql_compile_inline(char *fc_proc_source)
{
	char	   *fc_func_name = "inline_code_block";
	PLpgSQL_function *fc_function;
	ErrorContextCallback fc_plerrcontext;
	PLpgSQL_variable *fc_var;
	int			fc_parse_rc;
	MemoryContext fc_func_cxt;

	/*
	 * 设置扫描器输入和错误信息。我们假设此函数
	 * 不能递归调用，因此无需保存和恢复
	 * 此处使用的静态变量。
	 */
	plpgsql_scanner_init(fc_proc_source);

	plpgsql_error_funcname = fc_func_name;

	/*
	 * 为 ereport() 设置错误回溯支持。
	 */
	fc_plerrcontext.callback = fc_plpgsql_compile_error_callback;
	fc_plerrcontext.arg = fc_proc_source;
	fc_plerrcontext.previous = error_context_stack;
	error_context_stack = &fc_plerrcontext;

	/* 如果check_function_bodies开启，进行额外的语法检查 */
	plpgsql_check_syntax = check_function_bodies;

	/* 函数结构在当前语句后不再存在 */
	fc_function = (PLpgSQL_function *) palloc0(sizeof(PLpgSQL_function));

	plpgsql_curr_compile = fc_function;

	/*
	 * 所有其他编译时存储（例如，解析树）保存在
	 * 自己的内存上下文中，这样可以轻松回收。
	 */
	fc_func_cxt = AllocSetContextCreate(CurrentMemoryContext,
									 "PL/pgSQL inline code context",
									 ALLOCSET_DEFAULT_SIZES);
	plpgsql_compile_tmp_cxt = MemoryContextSwitchTo(fc_func_cxt);

	fc_function->fn_signature = pstrdup(fc_func_name);
	fc_function->fn_is_trigger = PLPGSQL_NOT_TRIGGER;
	fc_function->fn_input_collation = InvalidOid;
	fc_function->fn_cxt = fc_func_cxt;
	fc_function->out_param_varno = -1; /* 设置没有 OUT 参数 */
	fc_function->resolve_option = plpgsql_variable_conflict;
	fc_function->print_strict_params = plpgsql_print_strict_params;

	/*
	 * 不要对内联代码进行额外验证，因为我们不想在运行时添加垃圾
	 */
	fc_function->extra_warnings = 0;
	fc_function->extra_errors = 0;

	fc_function->nstatements = 0;
	fc_function->requires_procedure_resowner = false;

	plpgsql_ns_init();
	plpgsql_ns_push(fc_func_name, PLPGSQL_LABEL_BLOCK);
	plpgsql_DumpExecTree = false;
	fc_plpgsql_start_datums();

	/* 设置为仿佛在返回VOID的函数中 */
	fc_function->fn_rettype = VOIDOID;
	fc_function->fn_retset = false;
	fc_function->fn_retistuple = false;
	fc_function->fn_retisdomain = false;
	fc_function->fn_prokind = PROKIND_FUNCTION;
	/* 这里对类型VOID有一些硬编码的知识 */
	fc_function->fn_retbyval = true;
	fc_function->fn_rettyplen = sizeof(int32);

	/*
	 * 记住函数是STABLE/IMMUTABLE。XXX在只读事务中将其设为真会更好吗？不清楚。
	 */
	fc_function->fn_readonly = false;

	/*
	 * 创建魔术 FOUND 变量。
	 */
	fc_var = plpgsql_build_variable("found", 0,
								 plpgsql_build_datatype(BOOLOID,
														-1,
														InvalidOid,
														NULL),
								 true);
	fc_function->found_varno = fc_var->dno;

	/*
	 * 现在解析函数的文本
	 */
	fc_parse_rc = plpgsql_yyparse();
	if (fc_parse_rc != 0)
		elog(ERROR, "plpgsql parser returned %d", fc_parse_rc);
	fc_function->action = plpgsql_parse_result;

	plpgsql_scanner_finish();

	/*
	 * 如果返回VOID（目前总是为真），我们允许控制
	 * 自然落到最后而不需要显式RETURN语句。
	 */
	if (fc_function->fn_rettype == VOIDOID)
		fc_add_dummy_return(fc_function);

	/*
	 * 完成函数的信息
	 */
	fc_function->fn_nargs = 0;

	fc_plpgsql_finish_datums(fc_function);

	/*
	 * 弹出错误上下文堆栈
	 */
	error_context_stack = fc_plerrcontext.previous;
	plpgsql_error_funcname = NULL;

	plpgsql_check_syntax = false;

	MemoryContextSwitchTo(plpgsql_compile_tmp_cxt);
	plpgsql_compile_tmp_cxt = NULL;
	return fc_function;
}


/*
 * 错误上下文回调，让我们提供一个调用堆栈追踪。
 * 如果我们正在验证或执行匿名代码块，函数
 * 源文本作为参数传递。
 */
static void fc_plpgsql_compile_error_callback(void *fc_arg)
{
	if (fc_arg)
	{
		/*
		 * 尝试将语法错误位置转换为原始
		 * CREATE FUNCTION或DO命令的引用文本。
		 */
		if (function_parse_error_transpose((const char *) fc_arg))
			return;

		/*
		 * 如果报告了语法错误位置则完成；否则我们必须
		 * 回退到“接近第N行”的报告。
		 */
	}

	if (plpgsql_error_funcname)
		errcontext("compilation of PL/pgSQL function \"%s\" near line %d",
				   plpgsql_error_funcname, plpgsql_latest_lineno());
}


/*
 * 为函数参数添加一个名称到函数的命名空间
 */
static void fc_add_parameter_name(PLpgSQL_nsitem_type fc_itemtype, int fc_itemno, const char *fc_name)
{
	/*
	 * 在添加名称之前，检查重复项。我们需要这个，即使
	 * functioncmds.c 有类似的检查，因为该代码明确
	 * 不会对冲突的IN和OUT参数名称发出警告。在
	 * plpgsql中，这些名称在同一个命名空间中，因此没有办法
	 * 进行消歧。
	 */
	if (plpgsql_ns_lookup(plpgsql_ns_top(), true,
						  fc_name, NULL, NULL,
						  NULL) != NULL)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
				 errmsg("parameter name \"%s\" used more than once",
						fc_name)));

	/* 好吧，添加名称 */
	plpgsql_ns_additem(fc_itemtype, fc_itemno, fc_name);
}

/*
 * 在给定函数的主体中添加一个虚拟RETURN语句
 */
static void fc_add_dummy_return(PLpgSQL_function *fc_function)
{
	/*
	 * 如果外部块有EXCEPTION子句，我们需要创建一个新的外部
	 * 块，因为添加的RETURN不应该像是位于
	 * EXCEPTION子句内。类似地，如果有标签，则将其包装在新的外部
	 * 块中，以便EXIT不会跳过RETURN。
	 */
	if (fc_function->action->exceptions != NULL ||
		fc_function->action->label != NULL)
	{
		PLpgSQL_stmt_block *fc_new;

		fc_new = palloc0(sizeof(PLpgSQL_stmt_block));
		fc_new->cmd_type = PLPGSQL_STMT_BLOCK;
		fc_new->stmtid = ++fc_function->nstatements;
		fc_new->body = list_make1(fc_function->action);

		fc_function->action = fc_new;
	}
	if (fc_function->action->body == NIL ||
		((PLpgSQL_stmt *) llast(fc_function->action->body))->cmd_type != PLPGSQL_STMT_RETURN)
	{
		PLpgSQL_stmt_return *fc_new;

		fc_new = palloc0(sizeof(PLpgSQL_stmt_return));
		fc_new->cmd_type = PLPGSQL_STMT_RETURN;
		fc_new->stmtid = ++fc_function->nstatements;
		fc_new->expr = NULL;
		fc_new->retvarno = fc_function->out_param_varno;

		fc_function->action->body = lappend(fc_function->action->body, fc_new);
	}
}


/*
 * plpgsql_parser_setup		为动态参数设置解析器钩子
 *
 * 注意：这个例程以及它为其准备的钩子函数逻辑上是
 * plpgsql解析的一部分。但它们实际上在函数执行期间运行，
 * 当我们准备评估尚未解析和计划的SQL查询或表达式时。
 */
void plpgsql_parser_setup(struct ParseState *fc_pstate, PLpgSQL_expr *fc_expr)
{
	fc_pstate->p_pre_columnref_hook = fc_plpgsql_pre_column_ref;
	fc_pstate->p_post_columnref_hook = fc_plpgsql_post_column_ref;
	fc_pstate->p_paramref_hook = fc_plpgsql_param_ref;
	/* 不需要使用 p_coerce_param_hook */
	fc_pstate->p_ref_hook_state = (void *) fc_expr;
}

/*
 * plpgsql_pre_column_ref		解析器回调，在解析ColumnRef之前
 */
static Node * fc_plpgsql_pre_column_ref(ParseState *fc_pstate, ColumnRef *fc_cref)
{
	PLpgSQL_expr *fc_expr = (PLpgSQL_expr *) fc_pstate->p_ref_hook_state;

	if (fc_expr->func->resolve_option == PLPGSQL_RESOLVE_VARIABLE)
		return fc_resolve_column_ref(fc_pstate, fc_expr, fc_cref, false);
	else
		return NULL;
}

/*
 * plpgsql_post_column_ref		解析器回调，在解析ColumnRef之后
 */
static Node * fc_plpgsql_post_column_ref(ParseState *fc_pstate, ColumnRef *fc_cref, Node *fc_var)
{
	PLpgSQL_expr *fc_expr = (PLpgSQL_expr *) fc_pstate->p_ref_hook_state;
	Node	   *fc_myvar;

	if (fc_expr->func->resolve_option == PLPGSQL_RESOLVE_VARIABLE)
		return NULL;			/* 我们已经发现没有匹配 */

	if (fc_expr->func->resolve_option == PLPGSQL_RESOLVE_COLUMN && fc_var != NULL)
		return NULL;			/* 这是一个表列，优先考虑这个 */

	/*
	 * 如果我们找到一个记录/行变量但无法匹配字段名，如果ColumnRef也没有核心解析，则抛出错误。在这种情况下，引用肯定会失败，抱怨记录/行变量可能比核心解析器的错误信息更切题。（很遗憾，我们在这里无法访问transformColumnRef的内部crerr状态，因为在与表名冲突的情况下，这可能仍然不如最有帮助的错误信息。）
	 */
	fc_myvar = fc_resolve_column_ref(fc_pstate, fc_expr, fc_cref, (fc_var == NULL));

	if (fc_myvar != NULL && fc_var != NULL)
	{
		/*
		 * 我们可以让核心解析器抛出这个错误，但我们可以添加比核心更有用的详细信息。
		 */
		ereport(ERROR,
				(errcode(ERRCODE_AMBIGUOUS_COLUMN),
				 errmsg("column reference \"%s\" is ambiguous",
						NameListToString(fc_cref->fields)),
				 errdetail("It could refer to either a PL/pgSQL variable or a table column."),
				 parser_errposition(fc_pstate, fc_cref->location)));
	}

	return fc_myvar;
}

/*
 * plpgsql_param_ref		parser callback for ParamRefs ($n symbols)
 */
static Node * fc_plpgsql_param_ref(ParseState *fc_pstate, ParamRef *fc_pref)
{
	PLpgSQL_expr *fc_expr = (PLpgSQL_expr *) fc_pstate->p_ref_hook_state;
	char		fc_pname[32];
	PLpgSQL_nsitem *fc_nse;

	snprintf(fc_pname, sizeof(fc_pname), "$%d", fc_pref->number);

	fc_nse = plpgsql_ns_lookup(fc_expr->ns, false,
							fc_pname, NULL, NULL,
							NULL);

	if (fc_nse == NULL)
		return NULL;			/* plpgsql中未知的名称 */

	return fc_make_datum_param(fc_expr, fc_nse->itemno, fc_pref->location);
}

/*
 * resolve_column_ref		尝试将ColumnRef解析为plpgsql变量
 *
 * 返回转换后的节点结构，如果未找到名称则返回NULL
 *
 * error_if_no_field指示，如果我们能够匹配记录/行名称但未找到字段名称匹配，是否抛出错误或安静地返回NULL。
 */
static Node * fc_resolve_column_ref(ParseState *fc_pstate, PLpgSQL_expr *fc_expr,
				   ColumnRef *fc_cref, bool fc_error_if_no_field)
{
	PLpgSQL_execstate *fc_estate;
	PLpgSQL_nsitem *fc_nse;
	const char *fc_name1;
	const char *fc_name2 = NULL;
	const char *fc_name3 = NULL;
	const char *fc_colname = NULL;
	int			fc_nnames;
	int			fc_nnames_scalar = 0;
	int			fc_nnames_wholerow = 0;
	int			fc_nnames_field = 0;

	/*
	 * 我们使用函数的当前状态来解析参数数据类型。这实际上是相当虚假的，因为当这些类型发生变化时，并没有更新计划的规定……
	 */
	fc_estate = fc_expr->func->cur_estate;

	/*----------
	 * 允许的语法有：
	 *
	 * A		标量变量引用或整行记录引用。
	 * A.B		合格的标量或整行引用，或字段引用。
	 * A.B.C	合格的记录字段引用。
	 * A.*		整行记录引用。
	 * A.B.*	合格的整行记录引用。
	 *----------
	 */
	switch (list_length(fc_cref->fields))
	{
		case 1:
			{
				Node	   *fc_field1 = (Node *) linitial(fc_cref->fields);

				Assert(IsA(fc_field1, String));
				fc_name1 = strVal(fc_field1);
				fc_nnames_scalar = 1;
				fc_nnames_wholerow = 1;
				break;
			}
		case 2:
			{
				Node	   *fc_field1 = (Node *) linitial(fc_cref->fields);
				Node	   *fc_field2 = (Node *) lsecond(fc_cref->fields);

				Assert(IsA(fc_field1, String));
				fc_name1 = strVal(fc_field1);

				/* 整行引用？ */
				if (IsA(fc_field2, A_Star))
				{
					/* 设置name2以防止与标量变量匹配 */
					fc_name2 = "*";
					fc_nnames_wholerow = 1;
					break;
				}

				Assert(IsA(fc_field2, String));
				fc_name2 = strVal(fc_field2);
				fc_colname = fc_name2;
				fc_nnames_scalar = 2;
				fc_nnames_wholerow = 2;
				fc_nnames_field = 1;
				break;
			}
		case 3:
			{
				Node	   *fc_field1 = (Node *) linitial(fc_cref->fields);
				Node	   *fc_field2 = (Node *) lsecond(fc_cref->fields);
				Node	   *fc_field3 = (Node *) lthird(fc_cref->fields);

				Assert(IsA(fc_field1, String));
				fc_name1 = strVal(fc_field1);
				Assert(IsA(fc_field2, String));
				fc_name2 = strVal(fc_field2);

				/* 整行引用？ */
				if (IsA(fc_field3, A_Star))
				{
					/* 设置name3以防止与标量变量匹配 */
					fc_name3 = "*";
					fc_nnames_wholerow = 2;
					break;
				}

				Assert(IsA(fc_field3, String));
				fc_name3 = strVal(fc_field3);
				fc_colname = fc_name3;
				fc_nnames_field = 2;
				break;
			}
		default:
			/* 名称太多，忽略 */
			return NULL;
	}

	fc_nse = plpgsql_ns_lookup(fc_expr->ns, false,
							fc_name1, fc_name2, fc_name3,
							&fc_nnames);

	if (fc_nse == NULL)
		return NULL;			/* plpgsql中未知的名称 */

	switch (fc_nse->itemtype)
	{
		case PLPGSQL_NSTYPE_VAR:
			if (fc_nnames == fc_nnames_scalar)
				return fc_make_datum_param(fc_expr, fc_nse->itemno, fc_cref->location);
			break;
		case PLPGSQL_NSTYPE_REC:
			if (fc_nnames == fc_nnames_wholerow)
				return fc_make_datum_param(fc_expr, fc_nse->itemno, fc_cref->location);
			if (fc_nnames == fc_nnames_field)
			{
				/* colname可能是此记录中的一个字段 */
				PLpgSQL_rec *fc_rec = (PLpgSQL_rec *) fc_estate->datums[fc_nse->itemno];
				int			fc_i;

				/* 搜索引用此字段的数据项 */
				fc_i = fc_rec->firstfield;
				while (fc_i >= 0)
				{
					PLpgSQL_recfield *fc_fld = (PLpgSQL_recfield *) fc_estate->datums[fc_i];

					Assert(fc_fld->dtype == PLPGSQL_DTYPE_RECFIELD &&
						   fc_fld->recparentno == fc_nse->itemno);
					if (strcmp(fc_fld->fieldname, fc_colname) == 0)
					{
						return fc_make_datum_param(fc_expr, fc_i, fc_cref->location);
					}
					fc_i = fc_fld->nextfield;
				}

				/*
				 * 我们不应该到这里，因为每个可能的合格字段引用的RECFIELD数据项应该在解析时构建。但是如果我们到了这里，就像字段未找到一样处理：抛出错误或返回NULL。
				 */
				if (fc_error_if_no_field)
					ereport(ERROR,
							(errcode(ERRCODE_UNDEFINED_COLUMN),
							 errmsg("record \"%s\" has no field \"%s\"",
									(fc_nnames_field == 1) ? fc_name1 : fc_name2,
									fc_colname),
							 parser_errposition(fc_pstate, fc_cref->location)));
			}
			break;
		default:
			elog(ERROR, "unrecognized plpgsql itemtype: %d", fc_nse->itemtype);
	}

	/* 名称格式与plpgsql变量类型不匹配 */
	return NULL;
}

/*
 * 列引用解析的助手：构建一个引用plpgsql数据项的Param，并确保该数据项列在表达式的paramnos中。
 */
static Node * fc_make_datum_param(PLpgSQL_expr *fc_expr, int fc_dno, int fc_location)
{
	PLpgSQL_execstate *fc_estate;
	PLpgSQL_datum *fc_datum;
	Param	   *fc_param;
	MemoryContext fc_oldcontext;

	/* 请参见resolve_column_ref中的注释 */
	fc_estate = fc_expr->func->cur_estate;
	Assert(fc_dno >= 0 && fc_dno < fc_estate->ndatums);
	fc_datum = fc_estate->datums[fc_dno];

	/*
	 * Bitmapset必须分配在函数的永久内存上下文中
	 */
	fc_oldcontext = MemoryContextSwitchTo(fc_expr->func->fn_cxt);
	fc_expr->paramnos = bms_add_member(fc_expr->paramnos, fc_dno);
	MemoryContextSwitchTo(fc_oldcontext);

	fc_param = makeNode(Param);
	fc_param->paramkind = PARAM_EXTERN;
	fc_param->paramid = fc_dno + 1;
	plpgsql_exec_get_datum_type_info(fc_estate,
									 fc_datum,
									 &fc_param->paramtype,
									 &fc_param->paramtypmod,
									 &fc_param->paramcollid);
	fc_param->location = fc_location;

	return (Node *) fc_param;
}


/* ----------
 * plpgsql_parse_word		扫描器调用此方法进行后处理
 *				任何单一的非保留关键字。
 *
 * word1是小写的/去引号的标识符；必须在该函数的
 * 长期内存上下文中进行palloc。
 *
 * yytxt是原始令牌文本；我们需要这个来检查引号，
 * 以便后续对非保留关键字的检查能够正常工作。
 *
 * 仅在lookup为真并且plpgsql_IdentifierLookup上下文允许的情况下，
 * 我们尝试将令牌识别为变量。
 *
 * 如果识别为变量，填充*wdatum并返回true；
 * 如果未识别，填充*word并返回false。
 * （注意：这两个指针实际上指向同一个联合体的成员，
 * 但出于表述原因我们将它们分开传递。）
 * ----------
 */
bool plpgsql_parse_word(char *fc_word1, const char *fc_yytxt, bool fc_lookup,
				   PLwdatum *fc_wdatum, PLword *fc_word)
{
	PLpgSQL_nsitem *fc_ns;

	/*
	 * 我们不应该在DECLARE部分查找变量。在SQL
	 * 表达式中，也没有必要这样做——查找将在
	 * 表达式编译时发生。
	 */
	if (fc_lookup && plpgsql_IdentifierLookup == IDENTIFIER_LOOKUP_NORMAL)
	{
		/*
		 * 在当前命名空间栈中进行查找
		 */
		fc_ns = plpgsql_ns_lookup(plpgsql_ns_top(), false,
							   fc_word1, NULL, NULL,
							   NULL);

		if (fc_ns != NULL)
		{
			switch (fc_ns->itemtype)
			{
				case PLPGSQL_NSTYPE_VAR:
				case PLPGSQL_NSTYPE_REC:
					fc_wdatum->datum = plpgsql_Datums[fc_ns->itemno];
					fc_wdatum->ident = fc_word1;
					fc_wdatum->quoted = (fc_yytxt[0] == '"');
					fc_wdatum->idents = NIL;
					return true;

				default:
					/* plpgsql_ns_lookup永远不应该返回其他内容 */
					elog(ERROR, "unrecognized plpgsql itemtype: %d",
						 fc_ns->itemtype);
			}
		}
	}

	/*
	 * 没有找到 - 到现在为止，这只是一个没有任何特殊含义的词。
	 */
	fc_word->ident = fc_word1;
	fc_word->quoted = (fc_yytxt[0] == '"');
	return false;
}


/* ----------
 * plpgsql_parse_dblword		同样的查找两个由点隔开的单词
 * ----------
 */
bool plpgsql_parse_dblword(char *fc_word1, char *fc_word2,
					  PLwdatum *fc_wdatum, PLcword *fc_cword)
{
	PLpgSQL_nsitem *fc_ns;
	List	   *fc_idents;
	int			fc_nnames;

	fc_idents = list_make2(makeString(fc_word1),
						makeString(fc_word2));

	/*
	 * 我们在DECLARE部分不应该做任何事情。在SQL表达式中，我们
	 * 真的只需要确保在需要时创建RECFIELD数据。对于
	 * 这个函数处理的所有情况，返回带有两个单词标识符字符串的
	 * T_DATUM是正确的做法。
	 */
	if (plpgsql_IdentifierLookup != IDENTIFIER_LOOKUP_DECLARE)
	{
		/*
		 * 在当前命名空间栈中进行查找
		 */
		fc_ns = plpgsql_ns_lookup(plpgsql_ns_top(), false,
							   fc_word1, fc_word2, NULL,
							   &fc_nnames);
		if (fc_ns != NULL)
		{
			switch (fc_ns->itemtype)
			{
				case PLPGSQL_NSTYPE_VAR:
					/* 块限定的标量变量引用。 */
					fc_wdatum->datum = plpgsql_Datums[fc_ns->itemno];
					fc_wdatum->ident = NULL;
					fc_wdatum->quoted = false; /* 未使用 */
					fc_wdatum->idents = fc_idents;
					return true;

				case PLPGSQL_NSTYPE_REC:
					if (fc_nnames == 1)
					{
						/*
						 * 第一个词是记录名称，因此第二个词可以
						 * 是此记录中的字段。我们无论如何都会构建
						 * 一个RECFIELD数据——任何错误稍后将被
						 * 检测到。
						 */
						PLpgSQL_rec *fc_rec;
						PLpgSQL_recfield *fc_new;

						fc_rec = (PLpgSQL_rec *) (plpgsql_Datums[fc_ns->itemno]);
						fc_new = plpgsql_build_recfield(fc_rec, fc_word2);

						fc_wdatum->datum = (PLpgSQL_datum *) fc_new;
					}
					else
					{
						/* 块限定的记录变量引用。 */
						fc_wdatum->datum = plpgsql_Datums[fc_ns->itemno];
					}
					fc_wdatum->ident = NULL;
					fc_wdatum->quoted = false; /* 未使用 */
					fc_wdatum->idents = fc_idents;
					return true;

				default:
					break;
			}
		}
	}

	/* 没有找到 */
	fc_cword->idents = fc_idents;
	return false;
}


/* ----------
 * plpgsql_parse_tripword		同样的查找三个由点隔开的单词
 * ----------
 */
bool plpgsql_parse_tripword(char *fc_word1, char *fc_word2, char *fc_word3,
					   PLwdatum *fc_wdatum, PLcword *fc_cword)
{
	PLpgSQL_nsitem *fc_ns;
	List	   *fc_idents;
	int			fc_nnames;

	/*
	 * 我们在DECLARE部分不应该做任何事情。在SQL表达式中，我们需要
	 * 确保在需要时创建RECFIELD数据，并且需要小心
	 * 有多少名称被报告为属于
	 * T_DATUM：第三个词可能是子字段引用，我们在这里并不关心。
	 */
	if (plpgsql_IdentifierLookup != IDENTIFIER_LOOKUP_DECLARE)
	{
		/*
		 * 在当前命名空间栈中进行查找。必须找到记录
		 * 引用，否则忽略。
		 */
		fc_ns = plpgsql_ns_lookup(plpgsql_ns_top(), false,
							   fc_word1, fc_word2, fc_word3,
							   &fc_nnames);
		if (fc_ns != NULL)
		{
			switch (fc_ns->itemtype)
			{
				case PLPGSQL_NSTYPE_REC:
					{
						PLpgSQL_rec *fc_rec;
						PLpgSQL_recfield *fc_new;

						fc_rec = (PLpgSQL_rec *) (plpgsql_Datums[fc_ns->itemno]);
						if (fc_nnames == 1)
						{
							/*
							 * 第一个词是记录名称，因此第二个词
							 * 可以是此记录中的字段（第三个词，
							 * 是子字段）。我们无论如何都会构建
							 * 一个RECFIELD数据——任何错误将被
							 * 稍后检测到。
							 */
							fc_new = plpgsql_build_recfield(fc_rec, fc_word2);
							fc_idents = list_make2(makeString(fc_word1),
												makeString(fc_word2));
						}
						else
						{
							/* 块限定的记录变量引用。 */
							fc_new = plpgsql_build_recfield(fc_rec, fc_word3);
							fc_idents = list_make3(makeString(fc_word1),
												makeString(fc_word2),
												makeString(fc_word3));
						}
						fc_wdatum->datum = (PLpgSQL_datum *) fc_new;
						fc_wdatum->ident = NULL;
						fc_wdatum->quoted = false; /* 未使用 */
						fc_wdatum->idents = fc_idents;
						return true;
					}

				default:
					break;
			}
		}
	}

	/* 没有找到 */
	fc_idents = list_make3(makeString(fc_word1),
						makeString(fc_word2),
						makeString(fc_word3));
	fc_cword->idents = fc_idents;
	return false;
}


/* ----------
 * plpgsql_parse_wordtype	扫描器发现了word%TYPE。word可以是
 *				变量名或基本类型。
 *
 * 返回数据类型结构，如果没有找到匹配的word，则返回NULL。
 * ----------
 */
PLpgSQL_type * plpgsql_parse_wordtype(char *fc_ident)
{
	PLpgSQL_type *fc_dtype;
	PLpgSQL_nsitem *fc_nse;
	TypeName   *fc_typeName;
	HeapTuple	fc_typeTup;

	/*
	 * 在当前命名空间栈中查找
	 */
	fc_nse = plpgsql_ns_lookup(plpgsql_ns_top(), false,
							fc_ident, NULL, NULL,
							NULL);

	if (fc_nse != NULL)
	{
		switch (fc_nse->itemtype)
		{
			case PLPGSQL_NSTYPE_VAR:
				return ((PLpgSQL_var *) (plpgsql_Datums[fc_nse->itemno]))->datatype;

				/* XXX 或许在这里允许REC/ROW？ */

			default:
				return NULL;
		}
	}

	/*
	 * 在命名空间栈中没有找到该word。尝试查找具有该名称的数据类型，
	 * 但忽略shell类型和复杂类型。
	 */
	fc_typeName = makeTypeName(fc_ident);
	fc_typeTup = LookupTypeName(NULL, fc_typeName, NULL, false);
	if (fc_typeTup)
	{
		Form_pg_type fc_typeStruct = (Form_pg_type) GETSTRUCT(fc_typeTup);

		if (!fc_typeStruct->typisdefined ||
			fc_typeStruct->typrelid != InvalidOid)
		{
			ReleaseSysCache(fc_typeTup);
			return NULL;
		}

		fc_dtype = fc_build_datatype(fc_typeTup, -1,
							   plpgsql_curr_compile->fn_input_collation,
							   fc_typeName);

		ReleaseSysCache(fc_typeTup);
		return fc_dtype;
	}

	/*
	 * 没有找到 - 到现在为止，这只是一个没有任何特殊含义的词。
	 */
	return NULL;
}


/* ----------
 * plpgsql_parse_cwordtype		对compositeword%TYPE进行相同查找
 * ----------
 */
PLpgSQL_type * plpgsql_parse_cwordtype(List *fc_idents)
{
	PLpgSQL_type *fc_dtype = NULL;
	PLpgSQL_nsitem *fc_nse;
	const char *fc_fldname;
	Oid			fc_classOid;
	HeapTuple	fc_classtup = NULL;
	HeapTuple	fc_attrtup = NULL;
	HeapTuple	fc_typetup = NULL;
	Form_pg_class fc_classStruct;
	Form_pg_attribute fc_attrStruct;
	MemoryContext fc_oldCxt;

	/* 避免在长期函数上下文中的内存泄漏 */
	fc_oldCxt = MemoryContextSwitchTo(plpgsql_compile_tmp_cxt);

	if (list_length(fc_idents) == 2)
	{
		/*
		 * 在当前命名空间栈中查找。我们不需要检查匹配的名称数量，
		 * 因为我们只会考虑标量变量。
		 */
		fc_nse = plpgsql_ns_lookup(plpgsql_ns_top(), false,
								strVal(linitial(fc_idents)),
								strVal(lsecond(fc_idents)),
								NULL,
								NULL);

		if (fc_nse != NULL && fc_nse->itemtype == PLPGSQL_NSTYPE_VAR)
		{
			fc_dtype = ((PLpgSQL_var *) (plpgsql_Datums[fc_nse->itemno]))->datatype;
			goto done;
		}

		/*
		 * 第一个word也可以是表名
		 */
		fc_classOid = RelnameGetRelid(strVal(linitial(fc_idents)));
		if (!OidIsValid(fc_classOid))
			goto done;
		fc_fldname = strVal(lsecond(fc_idents));
	}
	else if (list_length(fc_idents) == 3)
	{
		RangeVar   *fc_relvar;

		fc_relvar = makeRangeVar(strVal(linitial(fc_idents)),
							  strVal(lsecond(fc_idents)),
							  -1);
		/* 无法锁定关系 - 我们可能没有权限。 */
		fc_classOid = RangeVarGetRelid(fc_relvar, NoLock, true);
		if (!OidIsValid(fc_classOid))
			goto done;
		fc_fldname = strVal(lthird(fc_idents));
	}
	else
		goto done;

	fc_classtup = SearchSysCache1(RELOID, ObjectIdGetDatum(fc_classOid));
	if (!HeapTupleIsValid(fc_classtup))
		goto done;
	fc_classStruct = (Form_pg_class) GETSTRUCT(fc_classtup);

	/*
	 * 它必须是关系、序列、视图、物化视图、复合类型或外部表
	 */
	if (fc_classStruct->relkind != RELKIND_RELATION &&
		fc_classStruct->relkind != RELKIND_SEQUENCE &&
		fc_classStruct->relkind != RELKIND_VIEW &&
		fc_classStruct->relkind != RELKIND_MATVIEW &&
		fc_classStruct->relkind != RELKIND_COMPOSITE_TYPE &&
		fc_classStruct->relkind != RELKIND_FOREIGN_TABLE &&
		fc_classStruct->relkind != RELKIND_PARTITIONED_TABLE)
		goto done;

	/*
	 * 获取命名表字段及其类型
	 */
	fc_attrtup = SearchSysCacheAttName(fc_classOid, fc_fldname);
	if (!HeapTupleIsValid(fc_attrtup))
		goto done;
	fc_attrStruct = (Form_pg_attribute) GETSTRUCT(fc_attrtup);

	fc_typetup = SearchSysCache1(TYPEOID,
							  ObjectIdGetDatum(fc_attrStruct->atttypid));
	if (!HeapTupleIsValid(fc_typetup))
		elog(ERROR, "cache lookup failed for type %u", fc_attrStruct->atttypid);

	/*
	 * 找到这一点 - 在调用者的上下文中构建一个编译器类型结构并
	 * 返回它。请注意，我们将类型视为通过OID找到；在无效化期间不会
	 * 尝试重新查找类型名称。
	 */
	MemoryContextSwitchTo(fc_oldCxt);
	fc_dtype = fc_build_datatype(fc_typetup,
						   fc_attrStruct->atttypmod,
						   fc_attrStruct->attcollation,
						   NULL);
	MemoryContextSwitchTo(plpgsql_compile_tmp_cxt);

done:
	if (HeapTupleIsValid(fc_classtup))
		ReleaseSysCache(fc_classtup);
	if (HeapTupleIsValid(fc_attrtup))
		ReleaseSysCache(fc_attrtup);
	if (HeapTupleIsValid(fc_typetup))
		ReleaseSysCache(fc_typetup);

	MemoryContextSwitchTo(fc_oldCxt);
	return fc_dtype;
}

/* ----------
 * plpgsql_parse_wordrowtype	扫描器发现了word%ROWTYPE。
 *					因此word必须是表名。
 * ----------
 */
PLpgSQL_type * plpgsql_parse_wordrowtype(char *fc_ident)
{
	Oid			fc_classOid;
	Oid			fc_typOid;

	/*
	 * 查找关系。请注意，由于关系行类型与其关系具有相同的名称，
	 * 这可以同样被处理为类型查找；我们只使用关系查找代码路径，因为
	 * 这里抛出的错误传统上指的是关系而不是类型。
	 * 但我们将构建一个TypeName，以防我们需要重新查找该类型。
	 */
	fc_classOid = RelnameGetRelid(fc_ident);
	if (!OidIsValid(fc_classOid))
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_TABLE),
				 errmsg("relation \"%s\" does not exist", fc_ident)));

	/* 一些relkind缺少类型OID */
	fc_typOid = get_rel_type_id(fc_classOid);
	if (!OidIsValid(fc_typOid))
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("relation \"%s\" does not have a composite type",
						fc_ident)));

	/* 构建并返回行类型结构 */
	return plpgsql_build_datatype(fc_typOid, -1, InvalidOid,
								  makeTypeName(fc_ident));
}

/* ----------
 * plpgsql_parse_cwordrowtype	扫描器发现了compositeword%ROWTYPE。
 *			因此word必须是命名空间限定的表名。
 * ----------
 */
PLpgSQL_type * plpgsql_parse_cwordrowtype(List *fc_idents)
{
	Oid			fc_classOid;
	Oid			fc_typOid;
	RangeVar   *fc_relvar;
	MemoryContext fc_oldCxt;

	/*
	 * 与上面相同，这是一个关系查找，但如果我们
	 * 不在错误措辞上保持向后兼容，则可以是类型查找。
	 */
	if (list_length(fc_idents) != 2)
		return NULL;

	/* 避免在长期函数上下文中的内存泄漏 */
	fc_oldCxt = MemoryContextSwitchTo(plpgsql_compile_tmp_cxt);

	/* 查找关系名称。无法锁定 - 我们可能没有权限。 */
	fc_relvar = makeRangeVar(strVal(linitial(fc_idents)),
						  strVal(lsecond(fc_idents)),
						  -1);
	fc_classOid = RangeVarGetRelid(fc_relvar, NoLock, false);

	/* 一些relkind缺少类型OID */
	fc_typOid = get_rel_type_id(fc_classOid);
	if (!OidIsValid(fc_typOid))
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("relation \"%s\" does not have a composite type",
						strVal(lsecond(fc_idents)))));

	MemoryContextSwitchTo(fc_oldCxt);

	/* 构建并返回行类型结构 */
	return plpgsql_build_datatype(fc_typOid, -1, InvalidOid,
								  makeTypeNameFromNameList(fc_idents));
}

/*
 * plpgsql_build_variable - 构建给定数据类型的datum数组条目
 *
 * 返回的结构可能是PLpgSQL_var或PLpgSQL_rec
 * 取决于给定的数据类型，并通过
 * palloc分配。该结构会自动添加到当前的datum
 * 数组中，并可选择添加到当前命名空间。
 */
PLpgSQL_variable *
plpgsql_build_variable(const char *fc_refname, int fc_lineno, PLpgSQL_type *fc_dtype,
					   bool fc_add2namespace)
{
	PLpgSQL_variable *fc_result;

	switch (fc_dtype->ttype)
	{
		case PLPGSQL_TTYPE_SCALAR:
			{
				/* 普通标量数据类型 */
				PLpgSQL_var *fc_var;

				fc_var = palloc0(sizeof(PLpgSQL_var));
				fc_var->dtype = PLPGSQL_DTYPE_VAR;
				fc_var->refname = pstrdup(fc_refname);
				fc_var->lineno = fc_lineno;
				fc_var->datatype = fc_dtype;
				/* 其他字段保持为0，可能会被调用者更改 */

				/* 预设为NULL */
				fc_var->value = 0;
				fc_var->isnull = true;
				fc_var->freeval = false;

				plpgsql_adddatum((PLpgSQL_datum *) fc_var);
				if (fc_add2namespace)
					plpgsql_ns_additem(PLPGSQL_NSTYPE_VAR,
									   fc_var->dno,
									   fc_refname);
				fc_result = (PLpgSQL_variable *) fc_var;
				break;
			}
		case PLPGSQL_TTYPE_REC:
			{
				/* 复合类型 -- 构建记录变量 */
				PLpgSQL_rec *fc_rec;

				fc_rec = plpgsql_build_record(fc_refname, fc_lineno,
										   fc_dtype, fc_dtype->typoid,
										   fc_add2namespace);
				fc_result = (PLpgSQL_variable *) fc_rec;
				break;
			}
		case PLPGSQL_TTYPE_PSEUDO:
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("variable \"%s\" has pseudo-type %s",
							fc_refname, format_type_be(fc_dtype->typoid))));
			fc_result = NULL;		/* 保持编译器安静 */
			break;
		default:
			elog(ERROR, "unrecognized ttype: %d", fc_dtype->ttype);
			fc_result = NULL;		/* 保持编译器安静 */
			break;
	}

	return fc_result;
}

/*
 * 构建空的命名记录变量，并可选择将其添加到命名空间
 */
PLpgSQL_rec *
plpgsql_build_record(const char *fc_refname, int fc_lineno,
					 PLpgSQL_type *fc_dtype, Oid fc_rectypeid,
					 bool fc_add2namespace)
{
	PLpgSQL_rec *fc_rec;

	fc_rec = palloc0(sizeof(PLpgSQL_rec));
	fc_rec->dtype = PLPGSQL_DTYPE_REC;
	fc_rec->refname = pstrdup(fc_refname);
	fc_rec->lineno = fc_lineno;
	/* 其他字段保持为0，可能会被调用者更改 */
	fc_rec->datatype = fc_dtype;
	fc_rec->rectypeid = fc_rectypeid;
	fc_rec->firstfield = -1;
	fc_rec->erh = NULL;
	plpgsql_adddatum((PLpgSQL_datum *) fc_rec);
	if (fc_add2namespace)
		plpgsql_ns_additem(PLPGSQL_NSTYPE_REC, fc_rec->dno, fc_rec->refname);

	return fc_rec;
}

/*
 * 根据组件变量构建行变量数据结构。
 * 包含一个rowtupdesc，因为我们需要物化行结果。
 */
static PLpgSQL_row *
fc_build_row_from_vars(PLpgSQL_variable **fc_vars, int fc_numvars)
{
	PLpgSQL_row *fc_row;
	int			fc_i;

	fc_row = palloc0(sizeof(PLpgSQL_row));
	fc_row->dtype = PLPGSQL_DTYPE_ROW;
	fc_row->refname = "(unnamed row)";
	fc_row->lineno = -1;
	fc_row->rowtupdesc = CreateTemplateTupleDesc(fc_numvars);
	fc_row->nfields = fc_numvars;
	fc_row->fieldnames = palloc(fc_numvars * sizeof(char *));
	fc_row->varnos = palloc(fc_numvars * sizeof(int));

	for (fc_i = 0; fc_i < fc_numvars; fc_i++)
	{
		PLpgSQL_variable *fc_var = fc_vars[fc_i];
		Oid			fc_typoid;
		int32		fc_typmod;
		Oid			fc_typcoll;

		/* 行的成员变量永远不应该是常量 */
		Assert(!fc_var->isconst);

		switch (fc_var->dtype)
		{
			case PLPGSQL_DTYPE_VAR:
			case PLPGSQL_DTYPE_PROMISE:
				fc_typoid = ((PLpgSQL_var *) fc_var)->datatype->typoid;
				fc_typmod = ((PLpgSQL_var *) fc_var)->datatype->atttypmod;
				fc_typcoll = ((PLpgSQL_var *) fc_var)->datatype->collation;
				break;

			case PLPGSQL_DTYPE_REC:
				/* 不应该需要重新验证rectypeid... */
				fc_typoid = ((PLpgSQL_rec *) fc_var)->rectypeid;
				fc_typmod = -1;	/* 不知道typmod，是否使用过 */
				fc_typcoll = InvalidOid;	/* 复合类型没有排序规则 */
				break;

			default:
				elog(ERROR, "unrecognized dtype: %d", fc_var->dtype);
				fc_typoid = InvalidOid;	/* 保持编译器安静 */
				fc_typmod = 0;
				fc_typcoll = InvalidOid;
				break;
		}

		fc_row->fieldnames[fc_i] = fc_var->refname;
		fc_row->varnos[fc_i] = fc_var->dno;

		TupleDescInitEntry(fc_row->rowtupdesc, fc_i + 1,
						   fc_var->refname,
						   fc_typoid, fc_typmod,
						   0);
		TupleDescInitEntryCollation(fc_row->rowtupdesc, fc_i + 1, fc_typcoll);
	}

	return fc_row;
}

/*
 * 为指定记录变量的命名字段构建RECFIELD数据项
 *
 * 如果已经存在这样的数据项，则直接返回；我们不需要重复项。
 */
PLpgSQL_recfield *
plpgsql_build_recfield(PLpgSQL_rec *fc_rec, const char *fc_fldname)
{
	PLpgSQL_recfield *fc_recfield;
	int			fc_i;

	/* 搜索引用此字段的现有数据项 */
	fc_i = fc_rec->firstfield;
	while (fc_i >= 0)
	{
		PLpgSQL_recfield *fc_fld = (PLpgSQL_recfield *) plpgsql_Datums[fc_i];

		Assert(fc_fld->dtype == PLPGSQL_DTYPE_RECFIELD &&
			   fc_fld->recparentno == fc_rec->dno);
		if (strcmp(fc_fld->fieldname, fc_fldname) == 0)
			return fc_fld;
		fc_i = fc_fld->nextfield;
	}

	/* 没有，所以创建一个新数据项 */
	fc_recfield = palloc0(sizeof(PLpgSQL_recfield));
	fc_recfield->dtype = PLPGSQL_DTYPE_RECFIELD;
	fc_recfield->fieldname = pstrdup(fc_fldname);
	fc_recfield->recparentno = fc_rec->dno;
	fc_recfield->rectupledescid = INVALID_TUPLEDESC_IDENTIFIER;

	plpgsql_adddatum((PLpgSQL_datum *) fc_recfield);

	/* 现在我们可以将其链接到父链中 */
	fc_recfield->nextfield = fc_rec->firstfield;
	fc_rec->firstfield = fc_recfield->dno;

	return fc_recfield;
}

/*
 * plpgsql_build_datatype
 *		根据类型OID、typmod、排序规则和类型的解析名称构建PLpgSQL_type结构。
 *
 * 如果排序规则不是InvalidOid，则它将覆盖类型的默认排序规则。
 * 但如果数据类型是非排序的，则会忽略排序规则。
 *
 * origtypname是用户写的类型名称的解析形式。
 * 如果类型不能是复合类型，或者一开始是通过OID识别的（例如，它是一个函数参数类型），
 * 那么它可以是NULL。
 */
PLpgSQL_type * plpgsql_build_datatype(Oid fc_typeOid, int32 fc_typmod,
					   Oid fc_collation, TypeName *fc_origtypname)
{
	HeapTuple	fc_typeTup;
	PLpgSQL_type *fc_typ;

	fc_typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(fc_typeOid));
	if (!HeapTupleIsValid(fc_typeTup))
		elog(ERROR, "cache lookup failed for type %u", fc_typeOid);

	fc_typ = fc_build_datatype(fc_typeTup, fc_typmod, fc_collation, fc_origtypname);

	ReleaseSysCache(fc_typeTup);

	return fc_typ;
}

/*
 * 辅助子程序，以给定pg_type条目和其他细节构建PLpgSQL_type结构
 * （请参见plpgsql_build_datatype的注释）。
 */
static PLpgSQL_type *
fc_build_datatype(HeapTuple fc_typeTup, int32 fc_typmod,
			   Oid fc_collation, TypeName *fc_origtypname)
{
	Form_pg_type fc_typeStruct = (Form_pg_type) GETSTRUCT(fc_typeTup);
	PLpgSQL_type *fc_typ;

	if (!fc_typeStruct->typisdefined)
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_OBJECT),
				 errmsg("type \"%s\" is only a shell",
						NameStr(fc_typeStruct->typname))));

	fc_typ = (PLpgSQL_type *) palloc(sizeof(PLpgSQL_type));

	fc_typ->typname = pstrdup(NameStr(fc_typeStruct->typname));
	fc_typ->typoid = fc_typeStruct->oid;
	switch (fc_typeStruct->typtype)
	{
		case TYPTYPE_BASE:
		case TYPTYPE_ENUM:
		case TYPTYPE_RANGE:
		case TYPTYPE_MULTIRANGE:
			fc_typ->ttype = PLPGSQL_TTYPE_SCALAR;
			break;
		case TYPTYPE_COMPOSITE:
			fc_typ->ttype = PLPGSQL_TTYPE_REC;
			break;
		case TYPTYPE_DOMAIN:
			if (type_is_rowtype(fc_typeStruct->typbasetype))
				fc_typ->ttype = PLPGSQL_TTYPE_REC;
			else
				fc_typ->ttype = PLPGSQL_TTYPE_SCALAR;
			break;
		case TYPTYPE_PSEUDO:
			if (fc_typ->typoid == RECORDOID)
				fc_typ->ttype = PLPGSQL_TTYPE_REC;
			else
				fc_typ->ttype = PLPGSQL_TTYPE_PSEUDO;
			break;
		default:
			elog(ERROR, "unrecognized typtype: %d",
				 (int) fc_typeStruct->typtype);
			break;
	}
	fc_typ->typlen = fc_typeStruct->typlen;
	fc_typ->typbyval = fc_typeStruct->typbyval;
	fc_typ->typtype = fc_typeStruct->typtype;
	fc_typ->collation = fc_typeStruct->typcollation;
	if (OidIsValid(fc_collation) && OidIsValid(fc_typ->collation))
		fc_typ->collation = fc_collation;
	/* 检测类型是否是真数组或其域 */
	/* 注意：这仅用于决定是否应用expand_array */
	if (fc_typeStruct->typtype == TYPTYPE_BASE)
	{
		/*
		 * 这个测试应该包括get_element_type()检查的内容。我们还
		 * 不允许非可吐槽的数组类型（即oidvector和int2vector）。
		 */
		fc_typ->typisarray = (IsTrueArrayType(fc_typeStruct) &&
						   fc_typeStruct->typstorage != TYPSTORAGE_PLAIN);
	}
	else if (fc_typeStruct->typtype == TYPTYPE_DOMAIN)
	{
		/* 如果不是varlena，我们可以短路查找基本类型 */
		fc_typ->typisarray = (fc_typeStruct->typlen == -1 &&
						   fc_typeStruct->typstorage != TYPSTORAGE_PLAIN &&
						   OidIsValid(get_base_element_type(fc_typeStruct->typbasetype)));
	}
	else
		fc_typ->typisarray = false;
	fc_typ->atttypmod = fc_typmod;

	/*
	 * 如果它是命名的复合类型（或其上域），找到typcache
	 * 条目并记录当前tupdesc ID，以便我们可以检测更改
	 * （包括删除）。我们目前不支持对非复合类型的即时替换，
	 * 否则我们可能也想这样做。
	 */
	if (fc_typ->ttype == PLPGSQL_TTYPE_REC && fc_typ->typoid != RECORDOID)
	{
		TypeCacheEntry *fc_typentry;

		fc_typentry = lookup_type_cache(fc_typ->typoid,
									 TYPECACHE_TUPDESC |
									 TYPECACHE_DOMAIN_BASE_INFO);
		if (fc_typentry->typtype == TYPTYPE_DOMAIN)
			fc_typentry = lookup_type_cache(fc_typentry->domainBaseType,
										 TYPECACHE_TUPDESC);
		if (fc_typentry->tupDesc == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("type %s is not composite",
							format_type_be(fc_typ->typoid))));

		fc_typ->origtypname = fc_origtypname;
		fc_typ->tcache = fc_typentry;
		fc_typ->tupdesc_id = fc_typentry->tupDesc_identifier;
	}
	else
	{
		fc_typ->origtypname = NULL;
		fc_typ->tcache = NULL;
		fc_typ->tupdesc_id = 0;
	}

	return fc_typ;
}

/*
 *	plpgsql_recognize_err_condition
 *		检查条件名称并将其转换为SQLSTATE。
 *
 * 注意：在某些情况下，相同的条件名称在表中有多个
 * 条目。我们任意返回第一个匹配项。
 */
int plpgsql_recognize_err_condition(const char *fc_condname, bool fc_allow_sqlstate)
{
	int			fc_i;

	if (fc_allow_sqlstate)
	{
		if (strlen(fc_condname) == 5 &&
			strspn(fc_condname, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 5)
			return MAKE_SQLSTATE(fc_condname[0],
								 fc_condname[1],
								 fc_condname[2],
								 fc_condname[3],
								 fc_condname[4]);
	}

	for (fc_i = 0; exception_label_map[fc_i].label != NULL; fc_i++)
	{
		if (strcmp(fc_condname, exception_label_map[fc_i].label) == 0)
			return exception_label_map[fc_i].sqlerrstate;
	}

	ereport(ERROR,
			(errcode(ERRCODE_UNDEFINED_OBJECT),
			 errmsg("unrecognized exception condition \"%s\"",
					fc_condname)));
	return 0;					/* 保持编译器安静 */
}

/*
 * plpgsql_parse_err_condition
 *		为异常条件名称生成PLpgSQL_condition条目
 *
 * 这必须能够返回一个列表，因为在错误代码名称表中存在一些重复
 * 名称。
 */
PLpgSQL_condition *
plpgsql_parse_err_condition(char *fc_condname)
{
	int			fc_i;
	PLpgSQL_condition *fc_new;
	PLpgSQL_condition *fc_prev;

	/*
	 * XXX 最终我们希望在这里查找用户定义的异常名称
	 */

	/*
	 * OTHERS被表示为代码0（这将映射到'00000'，但我们
	 * 不需要将其表示为异常条件）。
	 */
	if (strcmp(fc_condname, "others") == 0)
	{
		fc_new = palloc(sizeof(PLpgSQL_condition));
		fc_new->sqlerrstate = 0;
		fc_new->condname = fc_condname;
		fc_new->next = NULL;
		return fc_new;
	}

	fc_prev = NULL;
	for (fc_i = 0; exception_label_map[fc_i].label != NULL; fc_i++)
	{
		if (strcmp(fc_condname, exception_label_map[fc_i].label) == 0)
		{
			fc_new = palloc(sizeof(PLpgSQL_condition));
			fc_new->sqlerrstate = exception_label_map[fc_i].sqlerrstate;
			fc_new->condname = fc_condname;
			fc_new->next = fc_prev;
			fc_prev = fc_new;
		}
	}

	if (!fc_prev)
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_OBJECT),
				 errmsg("unrecognized exception condition \"%s\"",
						fc_condname)));

	return fc_prev;
}

/* ----------
 * plpgsql_start_datums			在编译启动时初始化datum列表。
 * ----------
 */
static void fc_plpgsql_start_datums(void)
{
	datums_alloc = 128;
	plpgsql_nDatums = 0;
	/* 这是短暂的，因此不需要在函数的上下文中分配 */
	plpgsql_Datums = MemoryContextAlloc(plpgsql_compile_tmp_cxt,
										sizeof(PLpgSQL_datum *) * datums_alloc);
	/* datums_last跟踪plpgsql_add_initdatums()所见的内容 */
	datums_last = 0;
}

/* ----------
 * plpgsql_adddatum			将变量、记录或行
 *					添加到编译器的datum列表中。
 * ----------
 */
void plpgsql_adddatum(PLpgSQL_datum *fc_newdatum)
{
	if (plpgsql_nDatums == datums_alloc)
	{
		datums_alloc *= 2;
		plpgsql_Datums = repalloc(plpgsql_Datums, sizeof(PLpgSQL_datum *) * datums_alloc);
	}

	fc_newdatum->dno = plpgsql_nDatums;
	plpgsql_Datums[plpgsql_nDatums++] = fc_newdatum;
}

/* ----------
 * plpgsql_finish_datums		将完成的datum信息复制到函数结构中。
 * ----------
 */
static void fc_plpgsql_finish_datums(PLpgSQL_function *fc_function)
{
	Size		fc_copiable_size = 0;
	int			fc_i;

	fc_function->ndatums = plpgsql_nDatums;
	fc_function->datums = palloc(sizeof(PLpgSQL_datum *) * plpgsql_nDatums);
	for (fc_i = 0; fc_i < plpgsql_nDatums; fc_i++)
	{
		fc_function->datums[fc_i] = plpgsql_Datums[fc_i];

		/* 这必须与copy_plpgsql_datums关于可复制内容一致 */
		switch (fc_function->datums[fc_i]->dtype)
		{
			case PLPGSQL_DTYPE_VAR:
			case PLPGSQL_DTYPE_PROMISE:
				fc_copiable_size += MAXALIGN(sizeof(PLpgSQL_var));
				break;
			case PLPGSQL_DTYPE_REC:
				fc_copiable_size += MAXALIGN(sizeof(PLpgSQL_rec));
				break;
			default:
				break;
		}
	}
	fc_function->copiable_size = fc_copiable_size;
}


/* ----------
 * plpgsql_add_initdatums		创建一个数组，其中包含
 *					自上次调用此函数以来创建的所有可初始化datum的编号。
 *
 * 如果varnos为NULL，我们只需忘记自上次调用以来创建的任何datum条目。
 *
 * 这在DECLARE部分周围使用，以创建在块入口处必须初始化的datums列表。
 * 请注意，datums也可以在DECLARE之外的其他地方创建，例如通过FOR循环，但
 * 然后由专用代码负责初始化它们。
 * ----------
 */
int plpgsql_add_initdatums(int **fc_varnos)
{
	int			fc_i;
	int			fc_n = 0;

	/*
	 * 此处识别的dtypes集合必须与exec_stmt_block()
	 * 在块入口时关心的（重新）初始化内容匹配。
	 */
	for (fc_i = datums_last; fc_i < plpgsql_nDatums; fc_i++)
	{
		switch (plpgsql_Datums[fc_i]->dtype)
		{
			case PLPGSQL_DTYPE_VAR:
			case PLPGSQL_DTYPE_REC:
				fc_n++;
				break;

			default:
				break;
		}
	}

	if (fc_varnos != NULL)
	{
		if (fc_n > 0)
		{
			*fc_varnos = (int *) palloc(sizeof(int) * fc_n);

			fc_n = 0;
			for (fc_i = datums_last; fc_i < plpgsql_nDatums; fc_i++)
			{
				switch (plpgsql_Datums[fc_i]->dtype)
				{
					case PLPGSQL_DTYPE_VAR:
					case PLPGSQL_DTYPE_REC:
						(*fc_varnos)[fc_n++] = plpgsql_Datums[fc_i]->dno;

					default:
						break;
				}
			}
		}
		else
			*fc_varnos = NULL;
	}

	datums_last = plpgsql_nDatums;
	return fc_n;
}


/*
 * 为给定的函数调用计算哈希键
 *
 * 哈希键返回到调用者提供的存储位置*hashkey中。
 */
static void fc_compute_function_hashkey(FunctionCallInfo fc_fcinfo,
						 Form_pg_proc fc_procStruct,
						 PLpgSQL_func_hashkey *fc_hashkey,
						 bool fc_forValidator)
{
	/* 确保结构的任何未使用字节都是零 */
	MemSet(fc_hashkey, 0, sizeof(PLpgSQL_func_hashkey));

	/* 获取函数OID */
	fc_hashkey->funcOid = fc_fcinfo->flinfo->fn_oid;

	/* 获取调用上下文 */
	fc_hashkey->isTrigger = CALLED_AS_TRIGGER(fc_fcinfo);
	fc_hashkey->isEventTrigger = CALLED_AS_EVENT_TRIGGER(fc_fcinfo);

	/*
	 * 如果是DML触发器，则将触发器的OID包含在哈希中，以便每个触发器
	 * 使用获得不同的哈希条目，从而允许例如不同的关系
	 * 行类型或过渡表名称。在验证模式下，我们不知道
	 * 预期使用的关系或过渡表名称，因此我们将trigOid设为零；
	 * 在这种情况下构建的哈希条目将永远不会被
	 * 用于任何实际调用。
	 *
	 * 我们目前不需要以相同的方式区分不同的事件触发器使用，
	 * 因为在这种情况下，特殊参数变量的类型不会变化。
	 */
	if (fc_hashkey->isTrigger && !fc_forValidator)
	{
		TriggerData *fc_trigdata = (TriggerData *) fc_fcinfo->context;

		fc_hashkey->trigOid = fc_trigdata->tg_trigger->tgoid;
	}

	/* 获取输入排序规则，如果已知 */
	fc_hashkey->inputCollation = fc_fcinfo->fncollation;

	if (fc_procStruct->pronargs > 0)
	{
		/* 获取参数类型 */
		memcpy(fc_hashkey->argtypes, fc_procStruct->proargtypes.values,
			   fc_procStruct->pronargs * sizeof(Oid));

		/* 解析任何多态参数类型 */
		fc_plpgsql_resolve_polymorphic_argtypes(fc_procStruct->pronargs,
											 fc_hashkey->argtypes,
											 NULL,
											 fc_fcinfo->flinfo->fn_expr,
											 fc_forValidator,
											 NameStr(fc_procStruct->proname));
	}
}

/*
 * 这与标准的 resolve_polymorphic_argtypes() 函数相同，
 * 除了：
 * 1. 如果我们无法解析类型，我们将继续报告错误。
 * 2. 我们将 RECORD 类型的输入参数（而非输出参数）视为多态处理，
 *    如果我们能确定它们的实际输入类型，就用实际输入类型替换它们的类型。
 *    这使我们可以为传递给此类参数的每个命名复合类型创建一个单独的
 *    函数缓存条目。
 * 3. 在验证模式下，我们没有输入可以查看，所以假设多态参数是
 *    整数、整数数组或整数范围。
 */
static void fc_plpgsql_resolve_polymorphic_argtypes(int fc_numargs,
									 Oid *fc_argtypes, char *fc_argmodes,
									 Node *fc_call_expr, bool fc_forValidator,
									 const char *fc_proname)
{
	int			fc_i;

	if (!fc_forValidator)
	{
		int			fc_inargno;

		/* 正常情况，传递给标准例程 */
		if (!resolve_polymorphic_argtypes(fc_numargs, fc_argtypes, fc_argmodes,
										  fc_call_expr))
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("could not determine actual argument "
							"type for polymorphic function \"%s\"",
							fc_proname)));
		/* 此外，将 RECORD 输入（但不是输出）视为多态 */
		fc_inargno = 0;
		for (fc_i = 0; fc_i < fc_numargs; fc_i++)
		{
			char		fc_argmode = fc_argmodes ? fc_argmodes[fc_i] : PROARGMODE_IN;

			if (fc_argmode == PROARGMODE_OUT || fc_argmode == PROARGMODE_TABLE)
				continue;
			if (fc_argtypes[fc_i] == RECORDOID || fc_argtypes[fc_i] == RECORDARRAYOID)
			{
				Oid			fc_resolvedtype = get_call_expr_argtype(fc_call_expr,
																 fc_inargno);

				if (OidIsValid(fc_resolvedtype))
					fc_argtypes[fc_i] = fc_resolvedtype;
			}
			fc_inargno++;
		}
	}
	else
	{
		/* 特殊验证情况（对 RECORD 无需做任何事情） */
		for (fc_i = 0; fc_i < fc_numargs; fc_i++)
		{
			switch (fc_argtypes[fc_i])
			{
				case ANYELEMENTOID:
				case ANYNONARRAYOID:
				case ANYENUMOID:	/* XXX 可疑 */
				case ANYCOMPATIBLEOID:
				case ANYCOMPATIBLENONARRAYOID:
					fc_argtypes[fc_i] = INT4OID;
					break;
				case ANYARRAYOID:
				case ANYCOMPATIBLEARRAYOID:
					fc_argtypes[fc_i] = INT4ARRAYOID;
					break;
				case ANYRANGEOID:
				case ANYCOMPATIBLERANGEOID:
					fc_argtypes[fc_i] = INT4RANGEOID;
					break;
				case ANYMULTIRANGEOID:
					fc_argtypes[fc_i] = INT4MULTIRANGEOID;
					break;
				default:
					break;
			}
		}
	}
}

/*
 * delete_function - 尽可能清理过时的函数缓存
 *
 * 我们不能释放 PLpgSQL_function 结构本身，因为
 * 存在指向它的 fn_extra 指针的可能性。我们可以释放
 * 附属存储，但前提是没有正在进行的活动评估。
 * 否则我们将泄漏这些存储。由于这种情况只会发生在
 * 检测到 pg_proc 更新期间的嵌套递归调用中，因此泄漏似乎是可以接受的。
 *
 * 请注意，如果有多个 fn_extra 指针指向相同的函数缓存，
 * 则可以调用此函数多次。因此要小心不要重复操作。
 */
static void fc_delete_function(PLpgSQL_function *fc_func)
{
	/* 从哈希表中移除函数（可能已经完成） */
	fc_plpgsql_HashTableDelete(fc_func);

	/* 如果安全且尚未完成，则释放函数的存储 */
	if (fc_func->use_count == 0)
		plpgsql_free_function_memory(fc_func);
}

/* 可导出，以便我们可以从 _PG_init() 中调用它 */
void plpgsql_HashTableInit(void)
{
	HASHCTL		fc_ctl;

	/* 不要允许重复初始化 */
	Assert(plpgsql_HashTable == NULL);

	fc_ctl.keysize = sizeof(PLpgSQL_func_hashkey);
	fc_ctl.entrysize = sizeof(plpgsql_HashEnt);
	plpgsql_HashTable = hash_create("PLpgSQL function hash",
									FUNCS_PER_USER,
									&fc_ctl,
									HASH_ELEM | HASH_BLOBS);
}

static PLpgSQL_function *
fc_plpgsql_HashTableLookup(PLpgSQL_func_hashkey *fc_func_key)
{
	plpgsql_HashEnt *fc_hentry;

	fc_hentry = (plpgsql_HashEnt *) hash_search(plpgsql_HashTable,
											 (void *) fc_func_key,
											 HASH_FIND,
											 NULL);
	if (fc_hentry)
		return fc_hentry->function;
	else
		return NULL;
}

static void fc_plpgsql_HashTableInsert(PLpgSQL_function *fc_function,
						PLpgSQL_func_hashkey *fc_func_key)
{
	plpgsql_HashEnt *fc_hentry;
	bool		fc_found;

	fc_hentry = (plpgsql_HashEnt *) hash_search(plpgsql_HashTable,
											 (void *) fc_func_key,
											 HASH_ENTER,
											 &fc_found);
	if (fc_found)
		elog(WARNING, "trying to insert a function that already exists");

	fc_hentry->function = fc_function;
	/* 准备从函数到哈希表键的反向链接 */
	fc_function->fn_hashkey = &fc_hentry->key;
}

static void fc_plpgsql_HashTableDelete(PLpgSQL_function *fc_function)
{
	plpgsql_HashEnt *fc_hentry;

	/* 如果不在表中则不做任何事情 */
	if (fc_function->fn_hashkey == NULL)
		return;

	fc_hentry = (plpgsql_HashEnt *) hash_search(plpgsql_HashTable,
											 (void *) fc_function->fn_hashkey,
											 HASH_REMOVE,
											 NULL);
	if (fc_hentry == NULL)
		elog(WARNING, "trying to delete function that does not exist");

	/* 移除反向链接，该链接不再指向已分配的存储 */
	fc_function->fn_hashkey = NULL;
}
